﻿namespace NServiceBus.Core.Analyzer
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.CodeAnalysis;

    static class SymbolExtensions
    {
        public static string GetFullName(this IMethodSymbol method)
        {
            var tokens = new Stack<string>();
            tokens.Push(method.Name);

            var type = method.ContainingType;
            while (type != null)
            {
                tokens.Push(type.Name);
                type = type.ContainingType;
            }

            var @namespace = method.ContainingType.ContainingNamespace;
            while (!string.IsNullOrEmpty(@namespace?.Name))
            {
                tokens.Push(@namespace.Name);
                @namespace = @namespace.ContainingNamespace;
            }

            return string.Join(".", tokens);
        }

        public static bool Implements(this ITypeSymbol type, INamedTypeSymbol nonGenericInterface) =>
            type != null &&
            nonGenericInterface != null &&
            (type.Equals(nonGenericInterface, SymbolEqualityComparer.IncludeNullability) || type.AllInterfaces.Any(candidate => candidate.Equals(nonGenericInterface, SymbolEqualityComparer.IncludeNullability)));

        public static bool Extends(this IMethodSymbol method, INamedTypeSymbol nonGenericType)
        {
            if (method == null || nonGenericType == null)
            {
                return false;
            }

            if (!method.IsExtensionMethod)
            {
                return false;
            }

            if (method.ReducedFrom is not IMethodSymbol extensionMethod)
            {
                return false;
            }

            if (extensionMethod.Parameters.FirstOrDefault() is not IParameterSymbol thisParam)
            {
                return false;
            }

            return thisParam.Type.Equals(nonGenericType, SymbolEqualityComparer.IncludeNullability);
        }

        public static ITypeSymbol GetTypeSymbolOrDefault(this ISymbol symbol) => symbol switch
        {
            IDiscardSymbol symbolWithType => symbolWithType.Type,
            IEventSymbol symbolWithType => symbolWithType.Type,
            IFieldSymbol symbolWithType => symbolWithType.Type,
            ILocalSymbol symbolWithType => symbolWithType.Type,
            IMethodSymbol symbolWithType => symbolWithType.ReturnType,
            INamedTypeSymbol symbolWithType => symbolWithType,
            IParameterSymbol symbolWithType => symbolWithType.Type,
            IPointerTypeSymbol symbolWithType => symbolWithType.PointedAtType,
            IPropertySymbol symbolWithType => symbolWithType.Type,
            ITypeSymbol symbolWithType => symbolWithType,
            _ => null,
        };

        public static IEnumerable<ITypeSymbol> BaseTypesAndSelf(this ITypeSymbol type, bool includeInterfaces = false)
        {
            yield return type;

            for (
                var baseType = type.BaseType;
                baseType != null;
                baseType = baseType.BaseType)
            {
                yield return baseType;
            }

            if (includeInterfaces)
            {
                foreach (var iface in type.AllInterfaces)
                {
                    yield return iface;
                }
            }
        }

        public static bool IsSystemObjectType(this ITypeSymbol type) => type.SpecialType == SpecialType.System_Object;

        // TODO: cater for generics, including in/out - test what CA2016 does in .NET 6
        // after upgrading to Microsoft.CodeAnalysis.CSharp.Workspaces 3.x or later, we can convert this method to this single expression
        // see https://github.com/dotnet/roslyn-analyzers/blob/8236e8bdf092bd9ae21cf42d12b8c480459b5e36/src/Utilities/Compiler/Extensions/ITypeSymbolExtensions.cs#L15-L21
        // => fromSymbol != null && toSymbol != null && compilation.ClassifyCommonConversion(fromSymbol, toSymbol).IsImplicit;
        public static bool IsAssignableTo(
            this ITypeSymbol fromSymbol,
            ITypeSymbol toSymbol)
            => fromSymbol != null && toSymbol != null && fromSymbol.AllInterfaces.Concat(fromSymbol.BaseTypesAndSelf()).Any(candidate => candidate.Equals(toSymbol, SymbolEqualityComparer.IncludeNullability));

        public static bool TypeCanAcceptWithNullability(this ITypeSymbol type, ITypeSymbol possiblyNullableTypeToAcceptValueFrom)
        {
            if (!type.Equals(possiblyNullableTypeToAcceptValueFrom, SymbolEqualityComparer.Default))
            {
                return false;
            }

            return type.NullableAnnotation switch
            {
                // Type is a non-nullable and can therefore not accept a nullable
                NullableAnnotation.NotAnnotated => possiblyNullableTypeToAcceptValueFrom.NullableAnnotation == NullableAnnotation.NotAnnotated,
                // Target is a nullable (or not from code that knows about nullable types, so it's nullable) it can accept either nullable or non-nullable
                NullableAnnotation.Annotated or NullableAnnotation.None => true,
                _ => throw new ArgumentException("Not expecting a non-annotated type expression."),
            };
        }
    }
}
